利用 box-shadow 畫出任何圖案


Posted by ArvinH on 2020-02-01

前言

大約在兩年前我曾經寫過一篇文章介紹如何用 CSS 繪圖 - 用 CSS 畫畫的小技巧,該文章的最後我有稍微提到我們能夠利用 CSS3 的 box-shadow 屬性來製造出 Pixel 風格的圖案。然而,所有圖案不都是由 pixel 組成的嗎?如果我們能夠用 box-shadow 畫出 Pixel Art,那只要 Pixel 數量足夠,size 夠細緻,應該是能夠繪製出任何圖形的吧?

不過在用 CSS 畫畫的小技巧這篇文章中,所製作的是比較簡單的文字,透過直接編輯 box-shadow 還在可接受範圍中,但如果是想要繪製複雜一點的人物角色,例如鋼鐵人src

iron-man

這如果一格一格手動對照,然後撰寫 box-shadow,比登天還難,更別提想繪製出比 Pixel Art 細緻一點的圖案了。

網路上對於 box-shadow 的運用,大多圍繞在 Pixel Art 的實作,例如 Una Kravets 的部落格 介紹了如何使用 SCSS 與陣列來產生 CSS Pixel Art;上面鋼鐵人原圖也是利用相同原理Pixelator 則是讓你能線上繪製自己喜歡的 Pixel Art,並且產生出對應的 box-shadow

好在 Codepen 上高手如雲,被我發現一篇利用 Angular 實作圖片轉 box-shadow 的版本,實作方式很有意思,我用 svelte 改寫了一個版本,今天就來分享一下實作細節!

先看個成果:

Gif Demo:
gif-demo

Live Demo:

可以下載這個範例圖來上傳,效果會比較好:demo-mario

Box-Shadow

在開始前,先複習一下 box-shadow,CSS3 的 box-shadow 屬性可以設定多個值,每個值代表著一個 box-shadow 的 x 位移(x-offset),y 位移(y-offset),陰影模糊半徑(shadow blur radii),陰影擴散半徑(shadow spread radii) 和顏色(color)。

由於 允許設置多個值可控制 X 與 Y 位移 這兩個特質,box-shadow 非常適合用來組合成圖片,尤其是 Pixel Art。以黑白相間的棋盤為例:

<div style="
    width: 40px;
    height: 40px;
    box-shadow: 0 40px #000, 40px 0 #000;
    border: 1px solid #000;
"></div>

box-shadow-explain

依照這個原理,就能組合成複雜一點的 Pixel Art:

但說實話,要手動撰寫 box-shadow 來組合出這個小愛心,大概就去掉半條命了,還是得依靠 Pixelator 來繪製並產生 CSS。

圖片轉 box-shadow 實作原理

在複習完 box-shadow 組成圖片的原理後,應該不難推斷出圖片轉 box-shadow 的做法。

概念上就是先定義出一個 grid system,將圖片切割成一塊一塊的單位,接著計算出每個單位區塊的 x-offset 與 y-offset,然後放入對應顏顏色,這樣就能組合出一個 unit 所對應的 box-shadow 值,依此類推把每個單位區塊都轉換完即可。

實作上的步驟比較繁瑣一些,但概念是相同的:

  • 利用 URL.createObjectURL(event.target.files[0]); 將圖片檔案轉換成 Image 物件。
  • 在 image onload 時,透過 canvas 2d context 的 drawImage() 函式將圖片繪製到 canvas 上。
  • 接著再以 canvas 2d context 的 getImageData 取得一個以一維陣列存放的圖片資訊。
  • 遍歷該一維陣列內的圖片資訊,組合出 box-shadow 的值。

上述步驟中,最關鍵的就是最後一點,canvas 2d context 的 getImageData 函式會回傳一個一維陣列 - Unit8ClampedArray,裡面包含了圖片每個 unit 的 RGBA 值(值段區間為 0 ~ 255)。

利用這個一維陣列,我們就可以知道載入的圖片有多少 unit(grid system),每個 unit 又各自是什麼顏色,進而推算出 box-shadow 每一個值的 x-offset、y-offset 與顏色。這也是為何我們需要先將圖片繪製到 Canvas 的原因。

關鍵程式碼如下:

const buildPixelArt = (pixelSize = 1, image, canvas, canvasContext) => {
  const width = image.width;
  const height = image.height;

  canvas.width = width;
  canvas.height = height;
  canvasContext.drawImage(image, 0, 0);

  const boxShadow = [];
  const { data: imageData } = canvasContext.getImageData(0, 0, width, height);

  for (let i = 0, n = imageData.length; i < n; i += 4) {
    var a = imageData[i + 3];
    if (a > 0) {
      const row = Math.ceil((i + 4) / 4 / width - 1);
      const col = (i + 4) / 4 - row * width + 1;
      boxShadow.push(
        col * pixelSize +
          "px " +
          row * pixelSize +
          "px " +
          getColor(imageData[i], imageData[i + 1], imageData[i + 2], a / 255)
      );
    }
  }

Unit8ClampedArray 陣列裡面,每四個 indices 為一單位,分別為該 unit 的 RGBA 值,所以在迴圈中我們以 4 為遞增單位,並以此為計算二維平面中 rowcol 的基礎。

計算出一維陣列內每個 unit 在二維平面上的行與列後,個別乘上定義好的 pixelSize,就能算出該 unit 在 box-shadow 值中的 x-offset 與 y-offset,然後聯同顏色值一起 push 到 boxShadow 陣列中。

最後利用預先寫好的 css template,將 boxShadow 整合進去即可產生需要的 css style:

 const generatedCss =
    "#pixel-art {\n" +
    "  height: " +
    height * pixelSize +
    "px;\n" +
    "  width: " +
    width * pixelSize +
    "px;\n" +
    "}\n" +
    "#pixel-art:after {\n" +
    '  content: "";\n' +
    "  position: absolute;\n" +
    "  width: " +
    pixelSize +
    "px;\n" +
    "  height: " +
    pixelSize +
    "px;\n" +
    "  box-shadow:\n" +
    "    " +
    boxShadow.join(",\n    ") +
    ";\n" +
    "}";
  return generatedCss;

載入產生的 CSS 後,就可以看到我們上傳的圖片重新以 box-shadow 的形式被重組在頁面上,單單一個 div 就能繪製出任何圖形!蠻酷的吧!

完整程式碼請到 CodeSandbox 上翻閱,大部分邏輯都在 PixelArtArea.svelte 元件與 buildPixelArt.js 這隻檔案,其餘 svelte 部分的程式碼也很好理解,不過我是第一次用 svelte,若有使用不當的地方歡迎指教!

注意事項

利用 box-shadow 繪圖基本上沒什麼實質意義,就只是好玩而已,千萬不要把這用到正式環境,效能之差會把使用者端的瀏覽器搞當機的。這次的範例也只能吃得下像素較小的圖片,若是上傳了較大的檔案,打開 Devtool 時就會發現你的頁面 crash 了...
另外,產生完的 CSS,範例中我是直接 append 到 head 下,所以若是在沒有重整頁面的狀況下上傳別的圖片,就會再度 append 新的 css 進去,久了以後 head 也會越來越肥。

結論

CSS 真的很有趣,能做出許多意料之外的事,雖然絕大多數沒什麼用處,但這種技術上的創意應用所帶來的興奮感,正是繁忙於日常的開發者們所需要的吧!

資料來源

  1. Una Kravets 的部落格
  2. Convert an image to CSS Box Shadows
  3. iron man

#css #box-shadow #drawing









Related Posts

【THM Walkthrough】Lateral Movement and Pivoting (1)

【THM Walkthrough】Lateral Movement and Pivoting (1)

Django aggregate and annotate

Django aggregate and annotate

改善 Macbook 蝶式鍵盤問題

改善 Macbook 蝶式鍵盤問題




Newsletter




Comments